<?php

namespace RedUNIT\Blackhole;

use RedUNIT\Blackhole as Blackhole;
use RedBeanPHP\Facade as R;
use RedBeanPHP\OODBBean as OODBBean;
use RedBeanPHP\Driver\RPDO as RPDO;
use RedBeanPHP\Logger\RDefault as RDefault;
use RedBeanPHP\RedException as RedException;
use RedBeanPHP\BeanHelper\SimpleFacadeBeanHelper as SimpleFacadeBeanHelper;
use RedBeanPHP\QueryWriter;
use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter;
use RedBeanPHP\QueryWriter\MySQL as MySQLQueryWriter;
use RedBeanPHP\QueryWriter\PostgreSQL as PostgresQueryWriter;

/**
 * Misc
 *
 * This test suite contains tests for a various functionalities
 * and scenarios. For more details please consult the document
 * section attached to each individual test method listed here.
 *
 * @file    RedUNIT/Blackhole/Misc.php
 * @desc    Tests various features that do not rely on a database connection.
 * @author  Gabor de Mooij and the RedBeanPHP Community
 * @license New BSD/GPLv2
 *
 * (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community.
 * This source file is subject to the New BSD/GPLv2 License that is bundled
 * with this source code in the file license.txt.
 */

class Misc extends Blackhole
{
	/*
	 * What drivers should be loaded for this test pack?
	 */
	public function getTargetDrivers()
	{
		return array( 'sqlite' );
	}

	/**
	 * Test whether we get the correct exception if we try to
	 * add a database we don't have a compatible QueryWriter for.
	 *
	 * @return void
	 */
	public function testUnsupportedDatabaseWriter()
	{
		$exception = NULL;
		try {
			R::addDatabase( 'x', 'blackhole:host=localhost;dbname=db', 'username', 'password' );
		} catch( \Exception $e ) {
			$exception = $e;
		}
		asrt( ( $exception instanceof RedException ), TRUE );
		asrt( $exception->getMessage(), 'Unsupported database (blackhole).' );
		$rpdo = new \TestRPO( new \MockPDO );
		asrt( @$rpdo->testCap( 'utf8mb4' ), FALSE );
	}

	/**
	 * Test pstr and pint functions
	 *
	 * @return void
	 */
	public function testPintPstr()
	{
		$x = pstr('test');
		asrt(is_array($x), TRUE);
		asrt($x[0],'test');
		asrt($x[1],\PDO::PARAM_STR);
		$x = pint(123);
		asrt(is_array($x), TRUE);
		asrt($x[0],123);
		asrt($x[1],\PDO::PARAM_INT);
	}

	/**
	 * Misc tests.
	 * 'Tests' almost impossible lines to test.
	 * Not sure if very useful.
	 *
	 * @return void
	 */
	public function testMisc()
	{
		$null = R::getDatabaseAdapter()->getDatabase()->stringifyFetches( TRUE );
		asrt( NULL, $null );
		R::getDatabaseAdapter()->getDatabase()->stringifyFetches( FALSE );
	}

	/**
	 * Test whether we can toggle enforcement of the RedBeanPHP
	 * naming policy.
	 *
	 * @return void
	 */
	public function testEnforceNamingPolicy()
	{
		\RedBeanPHP\Util\DispenseHelper::setEnforceNamingPolicy( FALSE );
		R::dispense('a_b');
		pass();
		\RedBeanPHP\Util\DispenseHelper::setEnforceNamingPolicy( TRUE );
		try {
			R::dispense('a_b');
			fail();
		} catch( \Exception $e ) {
			pass();
		}
	}

	/**
	 * Test R::csv()
	 *
	 * @return void
	 */
	public function testCSV()
	{
		\RedBeanPHP\Util\QuickExport::operation( 'test', TRUE, TRUE );
		R::nuke();
		$city = R::dispense('city');
		$city->name = 'city1';
		$city->region = 'region1';
		$city->population = '200k';
		R::store($city);
		$qe = new \RedBeanPHP\Util\QuickExport( R::getToolBox() );
		$out = $qe->csv( 'SELECT `name`, population FROM city WHERE region = :region ',
			array( ':region' => 'region1' ),
			array( 'city', 'population' ),
			'/tmp/cities.csv'
			);
		$out = preg_replace( '/\W/', '', $out );
		asrt( 'PragmapublicExpires0CacheControlmustrevalidatepostcheck0precheck0CacheControlprivateContentTypetextcsvContentDispositionattachmentfilenamecitiescsvContentTransferEncodingbinarycitypopulationcity1200k', $out );
	}

	/**
	 * Test whether sqlStateIn can detect lock timeouts.
	 *
	 * @return void
	 */
	public function testLockTimeoutDetection()
	{
		$queryWriter = new MySQLQueryWriter( R::getDatabaseAdapter() );
		asrt($queryWriter->sqlStateIn('HY000', array(QueryWriter::C_SQLSTATE_LOCK_TIMEOUT), array(0,'1205')), TRUE);
		$queryWriter = new PostgresQueryWriter( R::getDatabaseAdapter() );
		asrt($queryWriter->sqlStateIn('55P03', array(QueryWriter::C_SQLSTATE_LOCK_TIMEOUT), array(0,'')), TRUE);
	}

	/**
	 * Tests setOption
	 *
	 * @return void
	 */
	public function testSetOptionFalse()
	{
		$false = R::getDatabaseAdapter()->setOption( 'unknown', 1 );
		asrt( $false, FALSE );
	}

	/**
	 * Test whether we can use the JSONSerializable interface and
	 * whether old-style JSON is still the same (backwards compatibility).
	 *
	 * @return void
	 */
	public function testJSONSerialize()
	{
		$hotel = R::dispense( 'hotel' );
		$hotel->name = 'Overlook';
		$room = R::dispense( 'room' );
		$room->number = 237;
		$hotel->ownRoomList[] = $room;
		$shine = (string) $hotel;
		asrt( $shine, '{"id":0,"name":"Overlook"}' ); //basic JSON
		$shine = json_encode( $hotel->jsonSerialize() ); //As of PHP 5.4 json_encode() will call jsonSerializable
		asrt( $shine, '{"id":0,"name":"Overlook","ownRoom":[{"id":0,"number":237}]}' ); //should get full JSON
	}

	/**
	 * Tests max parameter binding.
	 *
	 * @return void
	 */
	public function testIntegerBindingMax()
	{
		if ( defined( 'HHVM_VERSION' ) ) return; //not for hhvm...
		$driver = new RPDO( 'test-sqlite-53', 'user', 'pass' );
		$max = $driver->getIntegerBindingMax();
		asrt( $max, 2147483647 );
		$driver = new RPDO( 'cubrid', 'user', 'pass' );
		$max = $driver->getIntegerBindingMax();
		asrt( $max, 2147483647 );
		$driver = new RPDO( 'other', 'user', 'pass' );
		$max = $driver->getIntegerBindingMax();
		asrt( $max, PHP_INT_MAX );
	}

	/**
	* Should not be able to pass invalid mode (must be 0 or 1).
	*
	*/
	public function testInvalidDebugModeException()
	{
		try {
			R::debug( TRUE, 6 );
			fail();
		} catch ( RedException $e ) {
			pass();
		}
		R::debug( FALSE );
	}

	/**
	 * Adding a database twice no longer allowed, causes confusion
	 * and possible damage.
	 */
	public function testAddingTwice()
	{
		testpack( 'Test adding DB twice.' );

		try {
			R::addDatabase( 'sqlite', '' );
			fail();
		} catch ( RedException $ex ) {
			pass();
		}
	}

	/**
	 * Tests whether getID never produces a notice.
	 *
	 * @return void
	 */
	public function testGetIDShouldNeverPrintNotice()
	{
		set_error_handler(function($err, $errStr){
			die('>>>>FAIL :'.$err.' '.$errStr);
		});
		$bean = new OODBBean;
		$bean->getID();
		restore_error_handler();
		pass();
	}

	/**
	 * Tests setProperty.
	 *
	 * @return void
	 */
	public function testSetProperty()
	{
		$bean = R::dispense( 'bean' );
		$bean->item = 2;
		$bean->ownBean = R::dispense( 'bean', 2 );
		R::store( $bean );
		$bean = $bean->fresh();
		$bean->ownBean;
		$bean->setProperty( 'ownBean', array(), FALSE, FALSE );
		asrt( count( $bean->ownBean ), 0 );
		asrt( count( $bean->getMeta( 'sys.shadow.ownBean' ) ), 2 );
		asrt( $bean->isTainted(), TRUE );
		$bean->setProperty( 'ownBean', array(), TRUE, FALSE );
		asrt( count( $bean->ownBean ), 0 );
		asrt( count( $bean->getMeta( 'sys.shadow.ownBean' ) ), 0 );
		asrt( $bean->isTainted(), TRUE );
		$bean = $bean->fresh();
		$bean->setProperty( 'ownBean', array(), TRUE, FALSE );
		asrt( count( $bean->ownBean ), 0 );
		asrt( count( $bean->getMeta( 'sys.shadow.ownBean' ) ), 0 );
		asrt( $bean->isTainted(), FALSE );
		$bean = $bean->fresh();
		$bean->setProperty( 'ownBean', array(), TRUE, TRUE );
		asrt( count( $bean->ownBean ), 0 );
		asrt( count( $bean->getMeta( 'sys.shadow.ownBean' ) ), 0 );
		asrt( $bean->isTainted(), TRUE );
	}

	/**
	 * Tests beansToArray().
	 *
	 * @return void
	 */
	public function testBeansToArray()
	{
		testpack('Test R::beansToArray method');
		$bean1 = R::dispense( 'bean' );
		$bean1->name = 'hello';
		$bean2 = R::dispense( 'bean' );
		$bean2->name = 'world';
		$beans = array( $bean1, $bean2 );
		$array = R::beansToArray( $beans );
		asrt( $array[0]['name'], 'hello' );
		asrt( $array[1]['name'], 'world' );
	}

	/**
	 * Test debugging with custom logger.
	 *
	 * @return void
	 */
	public function testDebugCustomLogger()
	{
		testpack( 'Test debug mode with custom logger' );
		$pdoDriver = new RPDO( R::getDatabaseAdapter()->getDatabase()->getPDO() );
		$customLogger = new \CustomLogger;
		$pdoDriver->setDebugMode( TRUE, $customLogger );
		$pdoDriver->Execute( 'SELECT 123' );
		asrt( count( $customLogger->getLogMessage() ), 1 );
		$pdoDriver->setDebugMode( TRUE, NULL );
		asrt( ( $pdoDriver->getLogger() instanceof RDefault ), TRUE );
		testpack( 'Test bean->getProperties method' );
		$bean = R::dispense( 'bean' );
		$bean->property = 'hello';
		$props = $bean->getProperties();
		asrt( isset( $props['property'] ), TRUE );
		asrt( $props['property'], 'hello' );

	}

	/**
	 * Test Facade transactions.
	 *
	 * @return void
	 *
	 * @throws\Exception
	 */
	public function testTransactionInFacade()
	{
		testpack( 'Test transaction in facade' );
		$bean = R::dispense( 'bean' );
		$bean->name = 'a';
		R::store( $bean );
		R::trash( $bean );
		R::freeze( TRUE );
		$bean = R::dispense( 'bean' );
		$bean->name = 'a';
		R::store( $bean );
		asrt( R::count( 'bean' ), 1 );
		R::trash( $bean );
		asrt( R::count( 'bean' ), 0 );
		$bean = R::dispense( 'bean' );
		$bean->name = 'a';
		$id = R::transaction( function() use( &$bean ) {
			return R::transaction( function() use( &$bean ) {
				return R::store( $bean );
			} );
		} );
		asrt( (int) $id, (int) $bean->id );
		R::trash( $bean );
		$bean = R::dispense( 'bean' );
		$bean->name = 'a';
		$id = R::transaction( function() use( &$bean ) {
			return R::store( $bean );
		} );
		asrt( (int) $id, (int) $bean->id );
		R::trash( $bean );
		$bean = R::dispense( 'bean' );
		$bean->name = 'a';
		try {
			R::transaction( function () use ( $bean ) {
				R::store( $bean );
				R::transaction( function () {
					throw new \Exception();
				} );
			} );
		} catch (\Exception $e ) {
			pass();
		}
		asrt( R::count( 'bean' ), 0 );
		$bean = R::dispense( 'bean' );
		$bean->name = 'a';
		try {
			R::transaction( function () use ( $bean ) {
				R::transaction( function () use ( $bean ) {
					R::store( $bean );
					throw new \Exception();
				} );
			} );
		} catch (\Exception $e ) {
			pass();
		}
		asrt( R::count( 'bean' ), 0 );
		$bean = R::dispense( 'bean' );
		$bean->name = 'a';
		try {
			R::transaction( function () use ( $bean ) {
				R::transaction( function () use ( $bean ) {
					R::store( $bean );
				} );
			} );
		} catch (\Exception $e ) {
			pass();
		}
		asrt( R::count( 'bean' ), 1 );
		R::freeze( FALSE );
		try {
			R::transaction( 'nope' );
			fail();
		} catch (\Exception $e ) {
			pass();
		}
		testpack( 'Test Camelcase 2 underscore' );
		$names = array(
			'oneACLRoute'              => 'one_acl_route',
			'ALLUPPERCASE'             => 'alluppercase',
			'clientServerArchitecture' => 'client_server_architecture',
			'camelCase'                => 'camel_case',
			'peer2peer'                => 'peer2peer',
			'fromUs4You'               => 'from_us4_you',
			'lowercase'                => 'lowercase',
			'a1A2b'                    => 'a1a2b',
		);
		$bean = R::dispense( 'bean' );
		foreach ( $names as $name => $becomes ) {
			$bean->$name = 1;
			asrt( isset( $bean->$becomes ), TRUE );
		}
		testpack( 'Misc Tests' );
		R::debug( 1 );
		flush();
		ob_start();
		R::exec( 'SELECT 123' );
		$out = ob_get_contents();
		ob_end_clean();
		flush();
		pass();
		asrt( ( strpos( $out, 'SELECT 123' ) !== FALSE ), TRUE );
		R::debug( 0 );
		flush();
		ob_start();
		R::exec( 'SELECT 123' );
		$out = ob_get_contents();
		ob_end_clean();
		flush();
		pass();
		asrt( $out, '' );
		R::debug( 0 );
		pass();
		testpack( 'test to string override' );
		$band = R::dispense( 'band' );
		$str = strval( $band );
		asrt( $str, 'bigband' );
		testpack( 'test whether we can use isset/set in model' );
		$band->setProperty( 'property1', 123 );
		asrt( $band->property1, 123 );
		asrt( $band->checkProperty( 'property1' ), TRUE );
		asrt( $band->checkProperty( 'property2' ), FALSE );
		$band = new \Model_Band;
		$bean = R::dispense( 'band' );
		$bean->property3 = 123;
		$band->loadBean( $bean );
		$bean->property4 = 345;
		$band->setProperty( 'property1', 123 );
		asrt( $band->property1, 123 );
		asrt( $band->checkProperty( 'property1' ), TRUE );
		asrt( $band->checkProperty( 'property2' ), FALSE );
		asrt( $band->property3, 123 );
		asrt( $band->property4, 345 );
		testpack( 'Can we pass a\PDO object to Setup?' );
		$pdo = new \PDO( 'sqlite:test.db' );
		R::addDatabase( 'pdo', $pdo );
		R::selectDatabase( 'pdo' );
		R::getCell('SELECT 123;');
		testpack( 'Test array interface of beans' );
		$bean = R::dispense( 'bean' );
		$bean->hello = 'hi';
		$bean->world = 'planet';
		asrt( $bean['hello'], 'hi' );
		asrt( isset( $bean['hello'] ), TRUE );
		asrt( isset( $bean['bye'] ), FALSE );
		$bean['world'] = 'sphere';
		asrt( $bean->world, 'sphere' );
		foreach ( $bean as $key => $el ) {
			if ( $el == 'sphere' || $el == 'hi' || $el == 0 ) {
				pass();
			} else {
				fail();
			}
			if ( $key == 'hello' || $key == 'world' || $key == 'id' ) {
				pass();
			} else {
				fail();
			}
		}
		asrt( count( $bean ), 3 );
		unset( $bean['hello'] );
		asrt( count( $bean ), 2 );
		asrt( count( R::dispense( 'countable' ) ), 1 );
		// Otherwise untestable...
		$bean->setBeanHelper( new SimpleFacadeBeanHelper() );
		R::getRedBean()->setBeanHelper( new SimpleFacadeBeanHelper() );
		pass();
		// Test whether properties like owner and shareditem are still possible
		testpack( 'Test Bean Interface for Lists' );
		$bean = R::dispense( 'bean' );
		// Must not be list, because first char after own is lowercase
		asrt( is_array( $bean->owner ), FALSE );
		// Must not be list, because first char after shared is lowercase
		asrt( is_array( $bean->shareditem ), FALSE );
		asrt( is_array( $bean->own ), FALSE );
		asrt( is_array( $bean->shared ), FALSE );
		asrt( is_array( $bean->own_item ), FALSE );
		asrt( is_array( $bean->shared_item ), FALSE );
		asrt( is_array( $bean->{'own item'} ), FALSE );
		asrt( is_array( $bean->{'shared Item'} ), FALSE );
	}

	public function testConv2Beans()
	{
		$row1 = array('id' => 1, 'title'=>'test');
		$row2 = array('id' => 2, 'title'=>'test2');
		$beans = R::convertToBeans('page', array($row1, $row2));
		asrt(count($beans), 2);
		asrt($beans[2]->title, 'test2');
	}

	/**
	* Test the most important invalid bean combinations.
	*
	* @return void
	*/
	public function testInvalidType()
	{
		$invalid = array(
			'book_page', //no link beans
			'a_b_c', //no prefix
			'a b', //no space
			'bean@', //no invalid symbols
			'bean#', //no invalid symbols
			'bean$', //sometimes used in DB, not allowed
			'__bean',//no prefixes
			'.bean', //no object notation
			'bean-item', //no dash
			'beanOther'); //no camelcase (uppercase because of file system issues)

		foreach( $invalid as $j ) {
			try {
				R::dispense( $j );
				fail();
			} catch( RedException $e ) {
				pass();
			}
		}
	}

	/**
	* Test whether batch still works if no IDs have been passed.
	*
	* @return void
	*/
	public function testBatch0()
	{
		$zero = R::batch( 'page', array() );
		asrt( is_array( $zero ), TRUE );
		asrt( count( $zero ), 0 );
		$zero = R::batch( 'page', FALSE );
		asrt( is_array( $zero ), TRUE );
		asrt( count( $zero ), 0 );
		$zero = R::batch( 'page', NULL);
		asrt( is_array( $zero ), TRUE );
		asrt( count( $zero ), 0 );
	}

	/**
	* Test whether connection failure does not reveal
	* credentials.
	*
	* @return void
	*/
	public function testConnect()
	{
		$driver = new RPDO( 'dsn:invalid', 'usr', 'psst' );
		try {
			$driver->connect();
			fail();
		}
		catch( \PDOException $e ) {
			asrt( strpos( $e->getMessage(), 'invalid' ), FALSE );
			asrt( strpos( $e->getMessage(), 'usr' ), FALSE );
			asrt( strpos( $e->getMessage(), 'psst' ), FALSE );
		}
	}

	/**
	* Test whether we can create an instant database using
	* R::setup().
	*
	* Probably only works on *NIX systems.
	*
	* @return void
	*/
	public function testSetup()
	{
		$tmpDir = sys_get_temp_dir();
		R::setup();
	}

	/**
	 * Test camelCase to snake_case conversions.
	 *
	 * @return void
	 */
	public function testCamel2Snake()
	{
		asrt( AQueryWriter::camelsSnake('bookPage'), 'book_page' );
		asrt( AQueryWriter::camelsSnake('FTP'), 'ftp' );
		asrt( AQueryWriter::camelsSnake('ACLRules'), 'acl_rules' );
		asrt( AQueryWriter::camelsSnake('SSHConnectionProxy'), 'ssh_connection_proxy' );
		asrt( AQueryWriter::camelsSnake('proxyServerFacade'), 'proxy_server_facade' );
		asrt( AQueryWriter::camelsSnake('proxySSHClient'), 'proxy_ssh_client' );
		asrt( AQueryWriter::camelsSnake('objectACL2Factory'), 'object_acl2_factory' );
		asrt( AQueryWriter::camelsSnake('bookItems4Page'), 'book_items4_page' );
		asrt( AQueryWriter::camelsSnake('book☀Items4Page'), 'book☀_items4_page' );
		asrt( R::uncamelfy('book☀Items4Page'), 'book☀_items4_page' );
		$array = R::uncamelfy( array( 'bookPage' => 1, 'camelCaseString' => 2, 'snakeCaseString' => array( 'dolphinCaseString' => 3 ) ) );
		asrt( isset( $array['book_page'] ), TRUE );
		asrt( isset( $array['camel_case_string'] ), TRUE );
		asrt( isset( $array['snake_case_string']['dolphin_case_string'] ), TRUE );
		$array = R::uncamelfy( array( 'oneTwo' => array( 'twoThree' => array( 'threeFour' => 1 ) ) ) );
		asrt( isset( $array['one_two']['two_three']['three_four'] ), TRUE );
	}

	/**
	 * Test camelCase to snake_case conversions.
	 *
	 * @return void
	 */
	public function testSnake2Camel()
	{
		asrt( AQueryWriter::snakeCamel('book_page'), 'bookPage' );
		asrt( AQueryWriter::snakeCamel('ftp'), 'ftp' );
		asrt( AQueryWriter::snakeCamel('acl_rules'), 'aclRules' );
		asrt( AQueryWriter::snakeCamel('ssh_connection_proxy'), 'sshConnectionProxy' );
		asrt( AQueryWriter::snakeCamel('proxy_server_facade'), 'proxyServerFacade' );
		asrt( AQueryWriter::snakeCamel('proxy_ssh_client'), 'proxySshClient' );
		asrt( AQueryWriter::snakeCamel('object_acl2_factory'), 'objectAcl2Factory' );
		asrt( AQueryWriter::snakeCamel('book_items4_page'), 'bookItems4Page' );
		asrt( AQueryWriter::snakeCamel('book☀_items4_page'), 'book☀Items4Page' );
		asrt( R::camelfy('book☀_items4_page'), 'book☀Items4Page' );
		$array = R::camelfy( array( 'book_page' => 1, 'camel_case_string' => 2, 'snake_case_string' => array( 'dolphin_case_string' => 3 )  ) );
		asrt( isset( $array['bookPage'] ), TRUE );
		asrt( isset( $array['camelCaseString'] ), TRUE );
		asrt( isset( $array['snakeCaseString']['dolphinCaseString'] ), TRUE );
		$array = R::camelfy( array( 'one_two' => array( 'two_three' => array( 'three_four' => 1 ) ) ) );
		asrt( isset( $array['oneTwo']['twoThree']['threeFour'] ), TRUE );
		//Dolphin mode
		asrt( AQueryWriter::snakeCamel('book_id', true), 'bookID' );
		$array = R::camelfy( array( 'bookid' => array( 'page_id' => 3 )  ), true );
		asrt( isset( $array['bookid']['pageID'] ), TRUE );
		asrt( AQueryWriter::snakeCamel('book_id', true), 'bookID' );
		$array = R::camelfy( array( 'book_id' => array( 'page_id' => 3 )  ), true );
		asrt( isset( $array['bookID']['pageID'] ), TRUE );
		$array = R::camelfy( array( 'book_ids' => array( 'page_id' => 3 )  ), true );
		asrt( isset( $array['bookIds']['pageID'] ), TRUE );
	}

	/**
	 * Test that init SQL is being executed upon setting PDO.
	 *
	 * @return void
	 */
	public function testRunInitCodeOnSetPDO()
	{
		$pdo = R::getToolBox()->getDatabaseAdapter()->getDatabase()->getPDO();
		$rpdo = new \RedBeanPHP\Driver\RPDO( $pdo );
		$rpdo->setEnableLogging(true);
		$logger = new \RedBeanPHP\Logger\RDefault\Debug;
		$logger->setMode( \RedBeanPHP\Logger\RDefault::C_LOGGER_ARRAY );
		$rpdo->setLogger( $logger );
		$rpdo->setInitQuery('SELECT 123');
		$rpdo->setPDO( $pdo );
		$found = $logger->grep('SELECT 123');
		asrt(count($found), 1);
		asrt($found[0], 'SELECT 123');
	}
}

